package com.zb.zeus.common.engine;

import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zb.zeus.common.core.exception.BaseException;
import com.zb.zeus.common.engine.config.CalcConfig;
import com.zb.zeus.common.engine.domain.CalcConst;
import com.zb.zeus.common.engine.domain.Calcable;
import freemarker.cache.FileTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.util.Assert;

import java.io.File;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 计算公式
 *
 * @author fujianjian
 * @since 2021/4/7 10:31
 */
@Slf4j
public class Calculator {

    private static LoadingCache<String, GroovyObject> OBJ_CACHE = CacheBuilder.newBuilder()
            .maximumSize(2000L).expireAfterAccess(1L, TimeUnit.DAYS)
            .build(new CacheLoader<String, GroovyObject>() {
                @Override
                public GroovyObject load(String formula) throws Exception {
                    return Calculator.getGroovyObject(formula);
                }
            });

    private static Template template = null;
    private static CalcConfig config;

    private static ThreadLocal<Map<String, Object>> context = ThreadLocal.withInitial(() -> {
        Map<String, Object> contextMap = Maps.newHashMap();
        contextMap.put(CalcConst.CALC_STACK, new Stack<String>());
        contextMap.put(CalcConst.SAME_LEVEL, Collections.EMPTY_LIST);
        return contextMap;
    });

    public static void setConfig(CalcConfig config) {
        Calculator.config = config;
    }

    public static void setCurrCalcable(CurEntry entry) {
        Map<String, Object> ctx = getContext();
        ctx.put(CalcConst.AD_NUM, entry.calcable.subjectName());
        ctx.put(CalcConst.CUR_OBJ, entry);
    }
    public static CurEntry getCurrCalcable() {
        return (CurEntry) MapUtils.getObject(getContext(), CalcConst.CUR_OBJ);
    }

    public static Object checkFormula(String formula, Object... args) {
        Stopwatch started = Stopwatch.createStarted();
        Object preCompileRet = null;
        Map<String, Object> context = Calculator.getContext();
        // 是不是校验
        context.put(CalcConst.CHECKED, true);
        try {
            Assert.isTrue(StringUtils.isNotEmpty(formula), "校验公式不能为空");
            Object[] argsArr = Arrays.copyOf(args, CalcConst.THREE);
            int len = args.length;
            if (len < CalcConst.THREE) {
                Arrays.fill(argsArr, len, CalcConst.THREE, CalcConst.ONE);
            }
            preCompileRet = Calculator.preCompileScript(formula, argsArr);
            return preCompileRet;
        } catch (Exception e) {
            log.error("check formula error, check not pass", e);
            throw new BaseException(e.getMessage(), e);
        } finally {
            Calculator.recycle();
            started.stop();
            log.info("check formula [{}] completed, pre compile ret:{}, time cost: {}ms",
                    formula, preCompileRet, started.elapsed(TimeUnit.MILLISECONDS));
        }
    }
    /**
     * 获取计算记录的值
     *
     * @param calcable
     * @return
     */
    public static BigDecimal getCalcableVal(Calcable calcable) {
        initDefaultConfig();
        if (!config.isForceCalc() && Objects.nonNull(calcable.getScore())) {
            return calcable.getScore();
        }
        pushStack(calcable);
        BigDecimal ret;
        if (StringUtils.isEmpty(calcable.getFormula())) {
            ret =  Objects.isNull(calcable.getScore()) ? BigDecimal.ZERO : calcable.getScore();
        } else {
            Object methodRet = executeFormula(calcable.getFormula(),
                    calcable.targetVal(), calcable.actualVal(), calcable.weightVal());
            ret = obj2BigDecimal(methodRet);
        }
        BigDecimal score = ret.setScale(0, BigDecimal.ROUND_DOWN);
        if (Objects.nonNull(calcable.weightVal()) && calcable.weightVal().compareTo(BigDecimal.ZERO) != 0) {
            score = Formula.min(Formula.max(calcable.weightVal().add(ret), BigDecimal.ZERO),
                    calcable.weightVal().multiply(config.getIncrFactor())).setScale(0, BigDecimal.ROUND_DOWN);
        }
        calcable.setScore(score);
        popStack();
        return score;
    }

    protected static boolean pushStack(Calcable calcable) {
        Stack<String> calcStack = (Stack<String>) MapUtils.getObject(getContext(), CalcConst.CALC_STACK);
        String stackName = calcable.getClass().getName() + calcable.getProjectName();
        if (calcStack.contains(stackName)) {
            throw new BaseException(String.format("计算项[%s]存在循环依赖,计算失败", calcable.getProjectName()));
        }
        calcStack.push(stackName);
        log.info("push stack:{}", stackName);
        return true;
    }

    protected static void popStack() {
        Stack<String> calcStack = (Stack<String>) MapUtils.getObject(getContext(), CalcConst.CALC_STACK);
        String pop = calcStack.pop();
        log.info("pop stack:{}", pop);
    }

    protected static Object executeFormula(String formula, Object... args) {
        try {
            formula = formula.replace(StringUtils.LF, StringUtils.EMPTY)
                    .replace(StringUtils.CR, StringUtils.EMPTY);
            Map<String, Object> context = getContext();
            String cacheKey = formula + Arrays.toString(args);
            Object cacheRet = MapUtils.getObject(context, cacheKey);
            if (Objects.nonNull(cacheRet)) {
                return cacheRet;
            }
            GroovyObject groovyObject = OBJ_CACHE.get(formula);
            Object[] argsArr = Arrays.copyOf(args, CalcConst.THREE);
            Object methodRet = groovyObject.invokeMethod(config.getMethodName(), argsArr);
            //log.info("groovy object invoke result: {}", methodRet);
            context.put(cacheKey, methodRet);
            return methodRet;
        } catch (Exception e) {
            log.error("execute formula[{}] value occur error", formula, e);
            throw new BaseException(e.getMessage(), e);
        }
    }

    /**
     * 获取freemark模板
     *
     * @return
     */
    private static Template getTemplate() {
        if (Objects.isNull(template)) {
            initDefaultConfig();
            try {
                String encoding = "UTF-8";
                Configuration configuration = new Configuration(Configuration.VERSION_2_3_29);
                File templateDir = new File(config.getScriptDir());
                File templateFile = new File(templateDir, config.getScriptName());
                if (templateFile.exists()) {
                    log.info("file abs path:{}", templateFile.getAbsolutePath());
                    configuration.setTemplateLoader(new FileTemplateLoader(templateDir));
                    return configuration.getTemplate(config.getScriptName(), encoding);
                } else {
                    log.error("out template file [{}] not exist, will be use inner template file", config.getScriptDir());
                    configuration.setClassLoaderForTemplateLoading(Thread.currentThread().getContextClassLoader(), StringUtils.EMPTY);
                    template = configuration.getTemplate(config.getScriptName(), encoding);
                }
            } catch (Exception e) {
                throw new BaseException("获取公式模板异常", e);
            }
        }
        return template;
    }

    /**
     * 编译公式
     *
     * @param formula
     */
    protected static Object preCompileScript(String formula, Object... args) throws Exception {
        formula = formula.replace(StringUtils.LF, StringUtils.EMPTY)
                .replace(StringUtils.CR, StringUtils.EMPTY);
        GroovyObject groovyObject = OBJ_CACHE.get(formula);
        Object[] argsArr = Arrays.copyOf(args, CalcConst.THREE);
        Object preCompileRet = groovyObject.invokeMethod(config.getMethodName(), argsArr);
        log.info("pre compile ret:{}", preCompileRet);
        return preCompileRet;
    }

    private static GroovyObject getGroovyObject(String formula) throws Exception {
        Template template = getTemplate();
        Map<String, Object> data = Maps.newHashMap();
        data.put("formula", formula);
        data.put("methodName", config.getMethodName());
        String clazzStr = FreeMarkerTemplateUtils.processTemplateIntoString(template, data);
        log.info("clazz content: {}", clazzStr);

        GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
        return (GroovyObject) groovyClassLoader.parseClass(clazzStr).newInstance();
    }

    /**
     * 数值对象数组转列表
     *
     * @param nums
     * @return
     */
    protected static List<BigDecimal> objNumArray2List(Object... nums) {
        List<BigDecimal> valList = Lists.newArrayListWithCapacity(nums.length);
        for (Object num : nums) {
            try {
                valList.add(obj2BigDecimal(num));
            } catch (Exception e) {
                log.error("convert num to val error, skip this val:{}", num, e);
            }
        }
        return valList;
    }

    protected static BigDecimal obj2BigDecimal(Object num) {
        Assert.notNull(num, "convert obj num cannot be null!!!");
        BigDecimal ret;
        if (num instanceof String) {
            ret = NumberUtils.createBigDecimal((String) num);
        } else if (num instanceof BigDecimal) {
            ret = (BigDecimal) num;
        } else if (num instanceof Number) {
            ret = BigDecimal.valueOf(((Number) num).doubleValue());
        } else {
            throw new RuntimeException(String.format("obj num=[%s] cannot convert", num));
        }
        return ret;
    }

    /**
     * 当前计算实例对象
     */
    @Data
    @Accessors(chain = true)
    public static class CurEntry implements Calcable {
        private Calcable calcable;
        private List<Calcable> broCalcable;

        @Override
        public String getFormula() {
            return calcable.getFormula();
        }

        @Override
        public String getProjectName() {
            return calcable.getProjectName();
        }

        @Override
        public List<Calcable> childCalcable() {
            return calcable.childCalcable();
        }

        @Override
        public String subjectName() {
            return calcable.subjectName();
        }

        @Override
        public BigDecimal getScore() {
            return calcable.getScore();
        }

        @Override
        public void setScore(BigDecimal score) {
            calcable.setScore(score);
        }

        @Override
        public BigDecimal actualVal() {
            return calcable.actualVal();
        }

        @Override
        public BigDecimal targetVal() {
            return calcable.targetVal();
        }

        @Override
        public BigDecimal weightVal() {
            return calcable.weightVal();
        }
    }


    /**
     * 初始化默认配置
     */
    private static synchronized void initDefaultConfig() {
        if (Objects.isNull(config)) {
            CalcConfig cc = new CalcConfig();
            cc.setForceCalc(true);
            cc.setMethodName("defaultCalc");
            cc.setScriptName("script/calc_script.txt");
            cc.setScriptDir("./");
            cc.setIncrFactor(BigDecimal.valueOf(1.2d));
            config = cc;
        }
    }

    public static void recycle() {
        context.remove();
    }


    /**
     * 刷新本地全部缓存
     */
    public static void refreshAllCache(String... keys) {
        String refreshKey = ArrayUtils.isEmpty(keys) ? keys[0] : "ALL";
        switch (refreshKey) {
            case "CALC":
                template = null;
                OBJ_CACHE.invalidateAll();
                break;
            default:
                //RedisOpsUtils.removeOrgTreeDTOList();
                template = null;
                OBJ_CACHE.invalidateAll();
                break;

        }
    }

    public static Map<String, Object> getContext() {
        return context.get();
    }

}
